package com.powerwyx.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.powerwyx.shortlink.project.common.convention.errorcode.BaseErrorCode;
import com.powerwyx.shortlink.project.common.convention.exception.ClientException;
import com.powerwyx.shortlink.project.common.convention.exception.ServiceException;
import com.powerwyx.shortlink.project.common.enums.ValidDateTypeEnum;
import com.powerwyx.shortlink.project.mapper.LinkGotoMapper;
import com.powerwyx.shortlink.project.mapper.LinkMapper;
import com.powerwyx.shortlink.project.pojo.dto.*;
import com.powerwyx.shortlink.project.pojo.entity.Link;
import com.powerwyx.shortlink.project.pojo.entity.LinkGoto;
import com.powerwyx.shortlink.project.pojo.vo.*;
import com.powerwyx.shortlink.project.service.LinkService;
import com.powerwyx.shortlink.project.utils.HashUtil;
import com.powerwyx.shortlink.project.utils.LinkUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.powerwyx.shortlink.project.common.constant.MQConstant.SHORT_LINK_STATS_EXCHANGE;
import static com.powerwyx.shortlink.project.common.constant.MQConstant.SHORT_LINK_STATS_KEY;
import static com.powerwyx.shortlink.project.common.constant.RedisConstant.*;

/**
 * @author 17585
 * @description 针对表【t_link】的数据库操作Service实现
 * @createDate 2024-07-05 21:26:37
 */
@Service
@Slf4j
public class LinkServiceImpl extends ServiceImpl<LinkMapper, Link>
        implements LinkService {

    @Autowired
    private RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;

    @Autowired
    private LinkMapper linkMapper;

    @Autowired
    private LinkGotoMapper linkGotoMapper;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RedissonClient redissonClient;

    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;



    /**
     * 生成短链接
     */
    private String generateSuffix(ShortLinkCreateDTO shortLinkCreateDTO) {
        int count = 0;
        String shortUri;
        while (count < 10) {
            String originUrl = shortLinkCreateDTO.getOriginUrl();
            originUrl += UUID.randomUUID().toString();
            shortUri = HashUtil.hashToBase62(originUrl);
            if (!shortUriCreateCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain + "/" + shortUri)) {
                return shortUri;
            }
            count++;
        }

        throw new ServiceException("短链接频繁生成，请稍后再试!");
    }


    @SneakyThrows
    private String getFavicon(String url) {

        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_OK) {
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }


    /**
     * 创建短链接
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ShortLinkCreateVO createShortLink(ShortLinkCreateDTO shortLinkCreateDTO) {
        if (shortLinkCreateDTO == null) {
            throw new ClientException(BaseErrorCode.PARAMS_ERROR);
        }
        //生成短链接
        String shortLinkSuffix = generateSuffix(shortLinkCreateDTO);
        //拼接完整短链接
        String fullShortUrl = createShortLinkDefaultDomain + "/" + shortLinkSuffix;
        Link link = BeanUtil.copyProperties(shortLinkCreateDTO, Link.class);

        //设置域名
        link.setDomain(createShortLinkDefaultDomain);
        //设置短链接
        link.setShortUri(shortLinkSuffix);
        //设置完整短链接
        link.setFullShortUrl(fullShortUrl);
        //设置原始链接对应网站图标
        link.setFavicon(getFavicon(shortLinkCreateDTO.getOriginUrl()));
        //设置默认启用
        link.setEnableStatus(0);
        link.setTotalPv(0);
        link.setTotalUv(0);
        link.setTotalUip(0);

        //创建LinkGoTO对象
        LinkGoto linkGoto = LinkGoto.builder()
                .gid(shortLinkCreateDTO.getGid())
                .fullShortUrl(fullShortUrl)
                .build();
        //存储短链接
        try {
            save(link);
            linkGotoMapper.insert(linkGoto);
        } catch (DuplicateKeyException ex) {
            //如果布隆过滤器误判,将当前误判的完整短链接添加至布隆过滤器
            log.warn("短链接:{} 重复", fullShortUrl);
            if(!shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl)){
                shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
            }
            throw new ServiceException("已存在该短链接，请重试!");
        }
        //将fullShortUrl对应的originUrl添加到缓存中，做缓存预热
        stringRedisTemplate.opsForValue()
                .set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                        shortLinkCreateDTO.getOriginUrl(),
                        LinkUtil.getLinkCacheValidDate(shortLinkCreateDTO.getValidDate()),
                        TimeUnit.MILLISECONDS);
        //将短链接添加至布隆过滤器
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        //判断短连接是否存在空指针缓存中，如果存在则删除
        String isNull = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(isNull)) {
            stringRedisTemplate.delete(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        }
        //构建对象并返回
        return ShortLinkCreateVO.builder()
                .gid(link.getGid())
                .originUrl(link.getOriginUrl())
                .fullShortUrl("http://" + link.getFullShortUrl())
                .build();
    }


    /**
     * 根据分组进行分页查询短链接
     */
    @Override
    public IPage<ShortLinkPageVO> shortLinkPage(ShortLinkPageDTO shortLinkPageDTO) {
        IPage<Link> resultPage = linkMapper.pageLink(shortLinkPageDTO);

        return resultPage.convert(link -> {
            ShortLinkPageVO result = BeanUtil.copyProperties(link, ShortLinkPageVO.class);
            result.setDomain("http://" + result.getDomain());
            return result;
        });
    }


    /**
     * 查询当前用户下各个分组内短链接数量
     */
    @Override
    public List<ShortLinkCountVO> getGroupShortLinkCount(List<String> gids) {
        if (gids == null || gids.size() == 0) {
            throw new ServiceException(BaseErrorCode.PARAMS_ERROR);
        }

        return linkMapper.getGroupShortLinkCount(gids);
    }





    /**
     * 修改短链接信息
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateShortLink(ShortLinkUpdateDTO shortLinkUpdateDTO) {
        if(shortLinkUpdateDTO == null){
            throw new ServiceException(BaseErrorCode.PARAMS_ERROR);
        }

        LambdaQueryWrapper<Link> queryWrapper = Wrappers.lambdaQuery(Link.class)
                .eq(Link::getGid, shortLinkUpdateDTO.getOriginGid())
                .eq(Link::getFullShortUrl, shortLinkUpdateDTO.getFullShortUrl())
                .eq(Link::getDelFlag, 0)
                .eq(Link::getEnableStatus, 0);
        Link hasShortLink = baseMapper.selectOne(queryWrapper);
        if (hasShortLink == null) {
            throw new ClientException("短链接不存在");
        }

        //如果没有修改分组
        if (Objects.equals(hasShortLink.getGid(), shortLinkUpdateDTO.getGid())) {
            Link link = Link.builder()
                    .domain(hasShortLink.getDomain())
                    .shortUri(hasShortLink.getShortUri())
                    .favicon(hasShortLink.getFavicon())
                    .createType(hasShortLink.getCreateType())
                    .gid(hasShortLink.getGid())
                    .originUrl(shortLinkUpdateDTO.getOriginUrl())
                    .describe(shortLinkUpdateDTO.getDescribe())
                    .validDateType(shortLinkUpdateDTO.getValidDateType())
                    .validDate(shortLinkUpdateDTO.getValidDate())
                    .build();

            LambdaUpdateWrapper<Link> updateWrapper = Wrappers.lambdaUpdate(Link.class)
                    .eq(Link::getFullShortUrl, shortLinkUpdateDTO.getFullShortUrl())
                    .eq(Link::getGid, shortLinkUpdateDTO.getGid())
                    .eq(Link::getDelFlag, 0)
                    .eq(Link::getEnableStatus, 0)
                    .set(Objects.equals(shortLinkUpdateDTO.getValidDateType(), ValidDateTypeEnum.PERMANENT.getType()), Link::getValidDate, null);
            baseMapper.update(link, updateWrapper);

        } else {
            //如果修改了分组,添加写锁，防止修改分组的时候，有用户进行短链接跳转，添加监控数据时查询到旧的分组id
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, shortLinkUpdateDTO.getFullShortUrl()));
            RLock wLock = readWriteLock.writeLock();
            wLock.lock();
            try {
                //修改短链接到新的分组
                Link link = Link.builder()
                        .domain(createShortLinkDefaultDomain)
                        .shortUri(hasShortLink.getShortUri())
                        .clickNumber(hasShortLink.getClickNumber())
                        .createType(hasShortLink.getCreateType())
                        .createType(hasShortLink.getCreateType())
                        .enableStatus(hasShortLink.getEnableStatus())
                        .todayPv(hasShortLink.getTodayPv())
                        .totalUv(hasShortLink.getTotalUv())
                        .todayUip(hasShortLink.getTodayUip())
                        .fullShortUrl(hasShortLink.getFullShortUrl())
                        .gid(shortLinkUpdateDTO.getGid())
                        .originUrl(shortLinkUpdateDTO.getOriginUrl())
                        .describe(shortLinkUpdateDTO.getDescribe())
                        .validDateType(shortLinkUpdateDTO.getValidDateType())
                        .validDate(shortLinkUpdateDTO.getValidDate())
                        .favicon(getFavicon(shortLinkUpdateDTO.getOriginUrl()))
                        .build();

                LambdaUpdateWrapper<Link> updateWrapper = Wrappers.lambdaUpdate(Link.class)
                        .eq(Link::getFullShortUrl, shortLinkUpdateDTO.getFullShortUrl())
                        .eq(Link::getGid, hasShortLink.getGid())
                        .eq(Link::getDelFlag, 0)
                        .eq(Link::getEnableStatus, 0);
                baseMapper.delete(updateWrapper);
                baseMapper.insert(link);

                //修改短链接路由表信息中完整短链接对应的新分组gid
                LinkGoto linkGoto = linkGotoMapper.selectOne(new QueryWrapper<LinkGoto>()
                        .eq("full_short_url", hasShortLink.getFullShortUrl()));
                linkGotoMapper.delete(new QueryWrapper<LinkGoto>()
                        .eq("full_short_url",hasShortLink.getFullShortUrl())
                        .eq("gid",hasShortLink.getGid()));
                linkGoto.setGid(shortLinkUpdateDTO.getGid());
                linkGotoMapper.insert(linkGoto);
            } finally {
                wLock.unlock();
            }
        }
        //如果修改了短链接的有效期或者是原始链接，则需要删除缓存，在下次短链接跳转时重构缓存
        if (!Objects.equals(hasShortLink.getValidDateType(), shortLinkUpdateDTO.getValidDateType())
                || !Objects.equals(hasShortLink.getValidDate(), shortLinkUpdateDTO.getValidDate())
                || !Objects.equals(hasShortLink.getOriginUrl(), shortLinkUpdateDTO.getOriginUrl())) {
            stringRedisTemplate.delete(String.format(GOTO_SHORT_LINK_KEY, shortLinkUpdateDTO.getFullShortUrl()));
            //如果被修改的短链接是过期的，并且修改为不过期的，则需要删除缓存的空指针
            if (hasShortLink.getValidDate() != null && hasShortLink.getValidDate().before(new Date())) {
                if (Objects.equals(shortLinkUpdateDTO.getValidDateType(), ValidDateTypeEnum.PERMANENT.getType()) ||
                        shortLinkUpdateDTO.getValidDate().after(new Date())) {
                    stringRedisTemplate.delete(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, shortLinkUpdateDTO.getFullShortUrl()));
                }
            }

        }


    }

    /**
     * 短链接跳转
     */
    @SneakyThrows
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void restoreUrl(String shortUri, HttpServletRequest request, HttpServletResponse response) {
        if (shortUri == null) {
            throw new ServiceException(BaseErrorCode.PARAMS_ERROR);
        }
        String serverName = request.getServerName();
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each -> ":" + each)
                .orElse("");
        String fullShortUrl = serverName + serverPort + "/" + shortUri;
        //查询缓存
        String originalUrl = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        //如果不为空
        if (StrUtil.isNotBlank(originalUrl)) {
            //添加监控记录
            shortLinkStatistics(fullShortUrl, request, response);
            //重定向到原始链接
            response.sendRedirect(originalUrl);
        } else {//如果为空
            //查询布隆过滤器fullShortUrl是否存在，如果不存在则直接返回，防止缓存穿透问题
            boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
            if (!contains) {
                response.sendRedirect("/page/notfound");
                return;
            }
            // 再查询空指针缓存，如果不为空，则返回，防止布隆过滤器误判
            String isNull = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(isNull)) {
                response.sendRedirect("/page/notfound");
                return;
            }

            RLock lock = redissonClient.getLock(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
            // 获取锁，防止缓存击穿问题
            lock.lock();
            try {
                // 因为并发情况下有可能拿到锁的时候上一个线程刚进行了缓存重构，所以需要二次查询
                originalUrl = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
                if (StrUtil.isNotBlank(originalUrl)) {
                    // 添加监控记录
                    shortLinkStatistics(fullShortUrl, request, response);
                    // 重定向到原始链接
                    response.sendRedirect(originalUrl);
                    return;
                }

                // 二次查询空指针缓存，如果不为空，则返回，防止布隆过滤器误判
                isNull = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
                if (StrUtil.isNotBlank(isNull)) {
                    response.sendRedirect("/page/notfound");
                    return;
                }


                // 如果依然为空，查询数据库，重构缓存
                // 根据完整短链接查询路由表，获取完整短链接对应的分组id
                LinkGoto linkGoto = linkGotoMapper.selectOne(new QueryWrapper<LinkGoto>()
                        .eq("full_short_url", fullShortUrl));
                if (linkGoto == null) {
                    // 如果为空，将当前fullShortUrl标记到缓存，防止缓存穿透
                    stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30L, TimeUnit.SECONDS);
                    response.sendRedirect("/page/notfound");
                    return;
                }
                // 根据分组id和完整短链接查询link表，获取原始链接
                Link link = getOne(new QueryWrapper<Link>()
                        .eq("gid", linkGoto.getGid())
                        .eq("full_short_url", fullShortUrl)
                        .eq("enable_status", 0)
                        .eq("del_flag", 0));

                // 如果数据不存在，或者数据已过期
                if (link == null || (link.getValidDate() != null && link.getValidDate().before(new Date()))) {
                    stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30L, TimeUnit.SECONDS);
                    response.sendRedirect("/page/notfound");
                    return;
                }
                // 如果数据存在,将原始链接添加到redis当中
                stringRedisTemplate.opsForValue().set(
                        String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                        link.getOriginUrl(),
                        LinkUtil.getLinkCacheValidDate(link.getValidDate()),
                        TimeUnit.MILLISECONDS
                 );
                // 添加监控记录
                shortLinkStatistics(fullShortUrl, request, response);
                // 重定向到原始链接
                response.sendRedirect(link.getOriginUrl());
            } finally {
                lock.unlock();
             }
        }
    }


    @Override
    public void shortLinkStatistics(String fullShortUrl, HttpServletRequest request, HttpServletResponse response) {
        //原子布尔类型
        AtomicBoolean uvFlag = new AtomicBoolean();
        Cookie[] cookies = request.getCookies();
        try {
            //原子引用
            AtomicReference<String> uv = new AtomicReference<>();
            Runnable addResponseCookieTask = () -> {
                uv.set(UUID.fastUUID().toString());
                Cookie uvCookie = new Cookie("uv", uv.get());
                //设置cookie有效期默认时间一个月
                uvCookie.setMaxAge(60 * 60 * 24 * 30);
                uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
                response.addCookie(uvCookie
                );
                uvFlag.set(Boolean.TRUE);
                stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV + fullShortUrl, uv.get());
            };

            if (ArrayUtil.isNotEmpty(cookies)) {
                Arrays.stream(cookies)
                        .filter(each -> Objects.equals(each.getName(), "uv"))
                        .findFirst()
                        .map(Cookie::getValue)
                        .ifPresentOrElse(each -> {
                            uv.set(each);
                            Long uvAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV + fullShortUrl, each);
                            uvFlag.set(uvAdded != null && uvAdded > 0L);
                        }, addResponseCookieTask);
            } else {
                addResponseCookieTask.run();
            }

            String remoteAddr = LinkUtil.getRealIp(request);
            Long uipAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UIP + fullShortUrl, remoteAddr);
            boolean uipFlag = uipAdded != null && uipAdded > 0L;

            String userFlag = uv.get();
            String os = LinkUtil.getOs(request);
            String browser = LinkUtil.getBrowser(request);
            String device = LinkUtil.getDevice(request);
            String network = LinkUtil.getNetwork(request);

            //添加监控记录
            ShortLinkStats shortLinkStats = ShortLinkStats.builder()
                    .fullShortUrl(fullShortUrl)
                    .uvFlag(uvFlag)
                    .uipFlag(uipFlag)
                    .remoteAddr(remoteAddr)
                    .userFlag(userFlag)
                    .os(os)
                    .browser(browser)
                    .device(device)
                    .network(network)
                    .currentDate(new Date())
                    .build();
            //发送消息到消息队列
            rabbitTemplate.convertAndSend(SHORT_LINK_STATS_EXCHANGE, SHORT_LINK_STATS_KEY, shortLinkStats);

        } catch (Exception e) {
            log.error("监控异常");
            throw new ClientException(BaseErrorCode.OPERATION_ERROR);
        }

    }


    /**
     * 批量创建短链接
     */
    @Override
    public ShortLinkBatchCreateVO batchCreateShortLink(ShortLinkBatchCreateDTO requestParam) {
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();
        List<ShortLinkBaseInfoVO> result = new ArrayList<>();
        for (int i = 0; i < originUrls.size(); i++) {
            ShortLinkCreateDTO shortLinkCreateDTO = BeanUtil.toBean(requestParam, ShortLinkCreateDTO.class);
            shortLinkCreateDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateDTO.setDescribe(describes.get(i));
            try {
                ShortLinkCreateVO shortLink = createShortLink(shortLinkCreateDTO);
                ShortLinkBaseInfoVO linkBaseInfoRespDTO = ShortLinkBaseInfoVO.builder()
                        .fullShortUrl(shortLink.getFullShortUrl())
                        .originUrl(shortLink.getOriginUrl())
                        .describe(describes.get(i))
                        .build();
                result.add(linkBaseInfoRespDTO);
            } catch (Throwable ex) {
                log.error("批量创建短链接失败，原始参数：{}", originUrls.get(i));
            }
        }
        return ShortLinkBatchCreateVO.builder()
                .total(result.size())
                .baseLinkInfos(result)
                .build();

    }



}

